import json as complexjson
from typing import Any, Union
from urllib.parse import urlencode

from pydantic import BaseModel
from playwright.async_api import Page
from loguru import logger


class MultipartFormData(BaseModel):
    name: str
    mimetype: str
    buffer: bytes


class Response:
    def __init__(self, url: str, buffer: Union[dict,list], status_code: int, headers: dict):
        self._url: str = url
        self._content: bytes = bytes(buffer) if isinstance(buffer,list) else bytes(list(buffer.values()))
        self._status_code: int = status_code
        self._headers = headers

    @property
    def ok(self):
        return 200 <= self._status_code <= 299

    @property
    def headers(self):
        return self._headers

    @property
    def url(self):
        return self._url

    @property
    def text(self) -> str:
        return self._content.decode()

    @property
    def content(self) -> bytes:
        return self._content

    def json(self, **kwargs):
        try:
            return complexjson.loads(self._content, **kwargs)
        except:
            return None

    @property
    def status_code(self) -> int:
        return self._status_code


class FetchAPI:
    @classmethod
    async def async_request(
            cls,
            page: Page,
            url_path,
            method='GET',
            params: Union[dict, list] = None,
            headers: dict = None,
            json: Any = None,
            data: Union[dict, str] = None,
            handle=None,
            form_data: dict = None
    ):
        """调用浏览器的 fetch api 来请求
        page        => playwright.async_api.Page 对象
        url_path    => 请求接口的路径，也可以是完整的url链接
        method      => 请求的方法
        params      => 请求的url参数
        headers     => 请求头
        json        => 请求的json参数，自动设置请求头的content-type为application/json
        data        => 请求的表单提交参数，自动设置请求头的content-type为application/x-www-form-urlencode
        form_data   => 使用 multipart/form-data 提交，一般用于上传文件
        handle      => 自定义处理 resp 的handle，使用 JS 语法,直接返回 JS 执行的结果，不再返回Response

        ################################### Example1 ###################################
        上传文件
        >>> resp = await FetchAPI.async_post(
        >>>     page,
        >>>     'https://httpbin.org/post',
        >>>     headers={"referrer": "/get"},
        >>>     form_data={
        >>>         "Doc-1": {"name": "1.txt", "buffer": b"123,ttt", "mimetype": "text/csv"},
        >>>         "document": "abc"
        >>>     }
        >>> )
        >>> resp.json()
        >>> resp.text
        ################################### Example2 ###################################
        下载文件
        >>> resp = await FetchAPI.async_get(page, '/image/png')
        >>> resp.content

        """
        logger.info(f"FetchAPI调用,method: {method.upper()}, url: {url_path} ")
        url = url_path
        if params:
            url += f"?{urlencode(params)}"

        if not headers:
            headers = {}

        body = None
        if method.upper() not in ['GET', 'HEAD', 'DELETE']:
            if json:
                body = complexjson.dumps(json)
                if not headers.get("content-type"):
                    headers.update({"content-type": "application/json"})
            elif data:
                if isinstance(data, dict):
                    body = urlencode(data)
                else:
                    body = data
                if not headers.get("content-type"):
                    headers.update({"content-type": "application/x-www-form-urlencoded"})
            elif form_data:
                fd_handle = await page.evaluate_handle('new FormData()')
                for name, value in form_data.items():
                    if isinstance(value, str) or isinstance(value, int) or isinstance(value, float):
                        await page.evaluate(
                            """([fd,name,value])=>Promise.resolve(fd.append(name,value))""",
                            [fd_handle, name, value]
                        )
                    elif isinstance(value, dict) or isinstance(value, MultipartFormData):
                        if isinstance(value, dict):
                            multipart_form_data = MultipartFormData(**value)
                        elif isinstance(value, MultipartFormData):
                            multipart_form_data = value
                        else:
                            raise AttributeError("from_data表单值类型不支持")

                        await page.evaluate(
                            """([fd, name, i_arr, mimetype, filename]) => Promise.resolve(fd.append(name, new Blob([new Uint8Array(i_arr)],{type: mimetype}), filename))""",
                            [
                                fd_handle,
                                name,
                                [_ for _ in multipart_form_data.buffer],
                                multipart_form_data.mimetype,
                                multipart_form_data.name
                            ]
                        )
                    else:
                        raise AttributeError("from_data表单值类型不支持")
                body = fd_handle

        referrer = headers.pop("referrer", None) or headers.pop("referer", None)

        init = {
            "method": method,
            "headers": headers
        }
        if referrer:
            init.update({"referrer": referrer})

        if body:
            init.update({"body": body})

        if not handle:
            handle = """let result = {};
                let headers = {};
                r.headers.forEach((v, k) => {
                    headers[k] = v;
                });
                result["status_code"] = r.status;
                result["buffer"] = new Uint8Array(await r.arrayBuffer());
                result["headers"] = headers;
                result["url"] = r.url;
                return result
            """

            fetch_js = f"""([url,init])=>fetch(url,init).then(async (r)=>{{{handle}}})"""
            response = await page.evaluate(fetch_js, [url, init])
            return Response(**response)
        else:
            fetch_js = f"""([url,init])=>fetch(url,init).then(async (r)=>{{{handle}}})"""
            return await page.evaluate(fetch_js, [url, init])

    @classmethod
    async def async_get(
            cls,
            page: Page,
            url_path,
            params: Union[dict, list] = None,
            headers: dict = None,
            handle=None,
    ):
        return await cls.async_request(
            page=page,
            url_path=url_path,
            params=params,
            headers=headers,
            handle=handle,
        )

    @classmethod
    async def async_post(
            cls,
            page: Page,
            url_path,
            params: Union[dict, list] = None,
            headers: dict = None,
            json: Any = None,
            data: Union[dict, str] = None,
            handle=None,
            form_data: dict = None,
    ):
        return await cls.async_request(
            page,
            url_path=url_path,
            method="post",
            params=params,
            headers=headers,
            json=json,
            data=data,
            handle=handle,
            form_data=form_data,
        )

    @classmethod
    async def async_put(
            cls,
            page: Page,
            url_path,
            params: Union[dict, list] = None,
            headers: dict = None,
            json: Any = None,
            data: Union[dict, str] = None,
            handle=None,
            form_data: dict = None,
    ):
        return await cls.async_request(
            page,
            url_path=url_path,
            method="put",
            params=params,
            headers=headers,
            json=json,
            data=data,
            handle=handle,
            form_data=form_data,
        )

    @classmethod
    async def async_delete(
            cls,
            page: Page,
            url_path,
            params: Union[dict, list] = None,
            headers: dict = None,
            json: Any = None,
            data: dict = None,
            handle=None,
            form_data: dict = None,
    ):
        return await cls.async_request(
            page,
            url_path=url_path,
            method="delete",
            params=params,
            headers=headers,
            json=json,
            data=data,
            handle=handle,
            form_data=form_data,
        )

    @staticmethod
    def api_request(url_path, method='GET'):
        def wrap(func):
            async def exec(*args, **kwargs):
                page: Page = args[0].pw.page
                result = await FetchAPI.async_request(
                    page,
                    url_path=url_path,
                    method=method,
                    params=kwargs.get("params"),
                    headers=kwargs.get("headers"),
                    json=kwargs.get("json"),
                    data=kwargs.get("data"),
                    handle=kwargs.get("handle"),
                )
                return await func(*args, result=result, **kwargs)

            return exec

        return wrap

    @staticmethod
    def api_get(url_path):
        def wrap(func):
            async def exec(*args, **kwargs):
                page: Page = args[0].pw.page
                result = await FetchAPI.async_get(
                    page,
                    url_path=url_path,
                    params=kwargs.get("params"),
                    headers=kwargs.get("headers"),
                    handle=kwargs.get("handle"),
                )
                return await func(*args, result=result, **kwargs)

            return exec

        return wrap

    @staticmethod
    def api_post(url_path):
        def wrap(func):
            async def exec(*args, **kwargs):
                page: Page = args[0].pw.page
                result = await FetchAPI.async_post(
                    page,
                    url_path=url_path,
                    params=kwargs.get("params"),
                    headers=kwargs.get("headers"),
                    json=kwargs.get("json"),
                    data=kwargs.get("data"),
                    handle=kwargs.get("handle"),
                    form_data=kwargs.get("form_data"),
                )
                return await func(*args, result=result, **kwargs)

            return exec

        return wrap

    @staticmethod
    def api_put(url_path):
        def wrap(func):
            async def exec(*args, **kwargs):
                page: Page = args[0].pw.page
                result = await FetchAPI.async_put(
                    page,
                    url_path=url_path,
                    params=kwargs.get("params"),
                    headers=kwargs.get("headers"),
                    json=kwargs.get("json"),
                    data=kwargs.get("data"),
                    handle=kwargs.get("handle"),
                    form_data=kwargs.get("form_data"),
                )
                return await func(*args, result=result, **kwargs)

            return exec

        return wrap

    @staticmethod
    def api_delete(url_path):
        def wrap(func):
            async def exec(*args, **kwargs):
                page: Page = args[0].pw.page
                result = await FetchAPI.async_delete(
                    page,
                    url_path=url_path,
                    params=kwargs.get("params"),
                    headers=kwargs.get("headers"),
                    json=kwargs.get("json"),
                    data=kwargs.get("data"),
                    handle=kwargs.get("handle"),
                    form_data=kwargs.get("form_data"),
                )
                return await func(*args, result=result, **kwargs)

            return exec

        return wrap
